package com.example.devecohp;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.ui.awt.RelativePoint;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Node;
import kotlin.Triple;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.awt.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;



/**
 * this class is registered in plugin.xml to provide the action of running the auditor on a single file
 */
public class Auditor extends AnAction {


    /**
     * the entry point for the auditor
     * @param e the AnActionEvent that carry the information of the action
     */
    @Override
    public void actionPerformed(AnActionEvent e) {
        Project project = e.getProject();
        PsiFile file = e.getData(CommonDataKeys.PSI_FILE);
        runAndCreatePageInfo(project,file);
        PageManager.getInstance(project).setModeReplace();

    }

    /**
     * run the auditor and create/ update the page infor object for a single file for auditor action
     * @param project the project the user is working on
     * @param file the file to audit
     */
    public void runAndCreatePageInfo(Project project, PsiFile file){
        ArrayList<PsiFile> fs = new ArrayList<PsiFile>(){};
        fs.add(file);
        runAndCreatePageInfo(project, fs, new createPageInfoCallback() {
                    @Override
                    public void run(PageInfo pageInfo) {
                        if (pageInfo == null) {
                            return;
                        }
                        RelativePoint r = RelativePoint.fromScreen(new Point(20000, 20000));
                        JComponent messageBox = new JLabel("successfully analyzed " + file.getName());
                        JBPopupFactory.getInstance().createBalloonBuilder(messageBox).createBalloon().
                                show(r, Balloon.Position.below);
                    }

                    @Override
                    public void onCancle() {}
                });
    }

    /**
     * get the auditor name, new file name and out put pattern of the given file path and file name
     * @param filePath the full path to the file
     * @param fileName the name of the file
     * @return a tuple containing the auditor that need to run, the name of the output file,and the pattern of the
     * output comment in that order
     */
    private static Triple<String,String,Pattern> checkFile(String filePath, String fileName){
        String auditorName;
        String newFilePath;
        Pattern pattern;
        if (fileName.endsWith(".ts")) {
            newFilePath = filePath.replaceFirst("\\.\\w+$", "") + ".audit.ts";
            auditorName = "hpaudit";
            pattern = Pattern.compile("/\\* HPAudit: (.*?) : (.*?) : (.*?) : (.*?) : (.*?) \\*/");
        } else if (fileName.endsWith(".js")) {
            newFilePath = filePath.replaceFirst("\\.\\w+$", "") + ".audit.js";
            auditorName = "hpaudit";
            pattern = Pattern.compile("/\\* HPAudit: (.*?) : (.*?) : (.*?) : (.*?) : (.*?) \\*/");
        }else if (fileName.endsWith(".ets")) {
            newFilePath = filePath.replaceFirst("\\.\\w+$", "") + ".audit.ets";
            auditorName = "hpaudit";
            pattern = Pattern.compile("/\\* HPAudit: (.*?) : (.*?) : (.*?) : (.*?) : (.*?) \\*/");
        } else if (fileName.endsWith(".c")) {
            newFilePath = filePath.replaceFirst("\\.\\w+$", "") + ".audit.c";
            auditorName = "cppaudit";
            pattern = Pattern.compile("/\\* CPPAudit: (.*?) : (.*?) : (.*?) : (.*?) : (.*?) \\*/");
        } else if (fileName.endsWith(".cpp")) {
            newFilePath = filePath.replaceFirst("\\.\\w+$", "") + ".audit.cpp";
            auditorName = "cppaudit";
            pattern = Pattern.compile("/\\* CPPAudit: (.*?) : (.*?) : (.*?) : (.*?) : (.*?) \\*/");
        } else {
            return new Triple<>(null,null,null);
        }
        return new Triple<>(auditorName,newFilePath,pattern);
    }
    /**
     * run the auditor then create a page info object or update the existing one and add it to the PageManager of the
     * project, it needs the user to have the auditor installed and the command ready
     * @param project the project the user is working on
     * @param files the files to run the auditor on
     * @param callback a callback function to call on the page Info object after everything is completed
     */
    public static void runAndCreatePageInfo(Project project, List<PsiFile>  files, createPageInfoCallback callback){
        runAndCreatePageInfo(project,files,callback,true);
    }
    public static void runAndCreatePageInfo(Project project, List<PsiFile>  files, createPageInfoCallback callback,boolean show_error){
        String separator = "\n";


        FileDocumentManager.getInstance().saveAllDocuments();
        ProgressManager.getInstance().run(new Task.Backgroundable(project, "Running HPAuditor", true) {
            @Override
            public void run(@NotNull ProgressIndicator indicator) {
                for(int j=0;j<files.size();j++){
                    PsiFile file = files.get(j);

                //for(PsiFile file :files) {
                    indicator.setFraction(j*1.0/files.size());
                    if(indicator.isCanceled()){
                        ToolWindowInfo toolWindowInfo = PageManager.getInstance(project).getToolWindowInfo();
                        toolWindowInfo.expandAll();
                        callback.onCancle();
                        //MyToolWindowFactory.updateUpperPanel(project);
                        //PageManager.getInstance(project).filterAllRules(toolWindowInfo.filterTextField.getText());
                        return;
                    }

                    if (project == null || file == null) {
                        callback.run(null);
                        continue;
                    }

                    String fileName = file.getName();
                    String filePath = file.getVirtualFile().getPath();
                    Triple<String, String, Pattern> result = checkFile(filePath, fileName);
                    // check the file type

                    String auditorName = result.getFirst();
                    String newFilePath = result.getSecond();
                    Pattern pattern = result.getThird();
                    if (auditorName == null || newFilePath == null || pattern == null) {
                        Messages.showInfoMessage("The plugin only works for c, cpp, ets and ts files", "Error");
                        callback.run(null);
                        continue;
                    }
                    StringBuilder hpauditOut = new StringBuilder();

                    List<PageInfo.RuleInfo> auditEntries = new ArrayList<>();
                    String[] cmds = new String[3];
                    cmds[0] = auditorName;
                    cmds[1] = filePath;
                    cmds[2] = "NOHPAUDITCOMMENT";
                    List<String> withoutComment = runAndGetOutput(cmds,show_error);
                    if (withoutComment == null) {
                        System.out.println(fileName);
                        callback.run(null);
                        continue;
                    }
                    List<String> withComment = runAndGetOutput(new String[]{auditorName, filePath},show_error);
                    if (withComment == null) {
                        System.out.println(fileName);
                        callback.run(null);
                        continue;
                    }
                    List<String> comments = diff(withoutComment, withComment);
                    for (String i : withoutComment) {
                        hpauditOut.append(i).append(separator);
                    }
                    Matcher matcher;
                    for (String line : comments) {
                        matcher = pattern.matcher(line);
                        if (matcher.find()) {
                            String lineNumber = matcher.group(4);
                            String ruleName = matcher.group(2);
                            String description = matcher.group(1);
                            int severity = Integer.parseInt(matcher.group(3));

                            // Construct the audit entry string
                            String auditEntry = description + " (" + ruleName + ") " + "[" + lineNumber + ", 0" + "]";

                            // Add the audit entry to the list
                            auditEntries.add(0, new PageInfo.RuleInfo(auditEntry, severity));
                        }
                    }

                    Application app = ApplicationManager.getApplication();
                    app.invokeLater(() -> {

                        VirtualFile newVirtualFile = new LightVirtualFile(newFilePath, hpauditOut.toString());
                        PageInfo pageInfo = PageManager.getInstance(project).createNewDiffPage(project, newVirtualFile, file, auditEntries);
                        callback.run(pageInfo);
                        ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow("HPAuditor");
                        updateDocumentPanel(PageManager.getInstance(project).getToolWindowInfo(), "");
                        toolWindow.activate(null);
                        //if the position is out of the window in the bottom right direction,
                        // it will be set to the bottom right of the window

                    });


                }
            }
        });
    }

    /**
     * go to the comparison file corresponding to filePath, and show the user the line corresponding to ruleMessage
     * @param ruleMessage the message displayed to the user about the rule
     * @param project the project the user is working on
     * @param filePath the full path of the file that the user running the auditor on
     */

    public static void jump(String ruleMessage, Project project,String filePath) throws IOException {
        Pattern parenthesesPattern = Pattern.compile("\\((.*?)\\)");
        Matcher parenthesesMatcher = parenthesesPattern.matcher(ruleMessage);
        String wordsInParentheses = null;

        if (parenthesesMatcher.find()) {
            wordsInParentheses = parenthesesMatcher.group(1);

            if (parenthesesMatcher.find()) {

                wordsInParentheses=parenthesesMatcher.group(1);
            }
        }
        PageInfo pageInfo = PageManager.getInstance(project).getPageInfo(filePath);
        jump(pageInfo.getNewLineNumber(ruleMessage)+1, wordsInParentheses, pageInfo.oldFile, project);

        }

    /**
     * go to the comparison file corresponding to file, and show the user the lineNum-th line
     * @param lineNum the line to show user
     * @param ruleId the rule corresponding to the line
     * @param file the file to show user
     * @param project the project the user is working on
     */
    public static void jump(int lineNum, String ruleId, PsiFile file, Project project) throws IOException{
        // jump to the certain line number
        if (lineNum == 0) {
            lineNum = 1;
        }
        PageInfo selected = PageManager.getInstance(project).getSelected();
        if(file!=null){
            selected= PageManager.getInstance(project).select(file);
        }
        selected.updateDiffPage(selected.getScrollRequest(lineNum));
        ToolWindowInfo toolWindowInfo = PageManager.getInstance(project).getToolWindowInfo();
        toolWindowInfo.languageOption.removeAllItems();
        for (String language : DocumentManager.getSupportedLanguage(ruleId)) {
            toolWindowInfo.languageOption.addItem(language);
        }
        updateDocumentPanel(toolWindowInfo, ruleId, DocumentManager.CHINESE);
        PageManager.getInstance(project).getSelected().setCurrRuleId(ruleId);

    }

    /**
     * Update the bottom right panel to show the document according to the current user setting
     * @param project the project to update
     * @param language the language the user chosen
     */
    public static void updateDocumentPanel(Project project, String language) {
        PageManager pageManager = PageManager.getInstance(project);
        updateDocumentPanel(pageManager.getToolWindowInfo(), pageManager.getSelected().getCurrRuleId(), language);
    }

    /**
     * Update the bottom right panel to show the document according to the current user setting
     * @param toolWindowInfo the toolWindowInfo for the tool window to update
     * @param ruleId the id of the rule selected
     * @param language the language the user selected
     */
    public static void updateDocumentPanel(ToolWindowInfo toolWindowInfo, String ruleId, String language){
        String mdContent ="";
        if(ruleId!=null) {
            mdContent = DocumentManager.getRuleContent(ruleId, language);
        }
         updateDocumentPanel(toolWindowInfo,mdContent);
    }

    /**
     * Update the bottom right panel to show the document
     * @param toolWindowInfo the toolWindowInfo for the tool window to update
     * @param mdContent the content to show in the panel, in Markdown format
     */
    public static void updateDocumentPanel(ToolWindowInfo toolWindowInfo, String mdContent) {
        Parser parser = Parser.builder().build();
        Node document = parser.parse(mdContent);

        HtmlRenderer renderer = HtmlRenderer.builder().build();
        String html = renderer.render(document);

        // Display HTML content in JEditorPane

        JEditorPane editorPane = new JEditorPane();
        editorPane.setContentType("text/html;charset=UTF_8");
        editorPane.putClientProperty("charset", "UTF_8");

        editorPane.setText(html);
        editorPane.setCaretPosition(0);
        editorPane.setEditable(false);

        JScrollPane scrollPane = new JScrollPane(editorPane);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        toolWindowInfo.secondarySplitter.setSecondComponent(scrollPane);

    }
    /**
     * find the elements in b that are not in a, assuming a and b are sorted
     * @param a the first list
     * @param b the second list
     * @return in b that are not in a, in the order of a and b
     */

    public static List<String> diff(List<String> a, List<String> b) {
        List<String> result = new ArrayList<>();
        int i = 0;
        int j = 0;
        while (i < a.size() && j < b.size()) {
            if (a.get(i).equals(b.get(j))) {
                i++;
            } else {
                result.add(b.get(j));
            }
            j++;
        }
        return result;
    }

    /**
     * run the command and get the output of it, also show error in a popup window if there are error
     * @param cmds the command to run
     * @return the lines the command outputs to stdout
     */
    public static ArrayList<String> runAndGetOutput(String[] cmds){
        return runAndGetOutput(cmds,true);
    }
    public static ArrayList<String> runAndGetOutput(String[] cmds,boolean show_error) {
        Process process;
        StringBuilder res = new StringBuilder();
        ArrayList<String> result = new ArrayList<>();
        try {
            process = Runtime.getRuntime().exec(cmds);
        } catch (IOException e) {
            if(show_error) {
                Application app = ApplicationManager.getApplication();
                app.invokeLater(() -> {
                    Messages.showErrorDialog(e.getMessage(), "Error");
                });
            }
            return null;
        }
        Charset encod = StandardCharsets.UTF_8;
        try (InputStream inputStream = process.getInputStream();
             InputStreamReader inputStreamReader = new InputStreamReader(inputStream, encod);
             BufferedReader bufferedReader = new BufferedReader(inputStreamReader)
        ) {
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                result.add(line);
            }

            if (process.exitValue() != 0) {

                InputStream stderr = process.getErrorStream();
                InputStreamReader isr = new InputStreamReader(stderr);
                BufferedReader errorBufferedReader = new BufferedReader(isr);

                while ((line = errorBufferedReader.readLine()) != null) {
                    res.append(line);
                }
                if (!res.toString().isEmpty()) {
                    if(show_error) {
                        Application app = ApplicationManager.getApplication();
                        app.invokeLater(() -> {
                            Messages.showErrorDialog(res.toString(), "Error");
                        });
                    }
                    return null;
                }
            }
            return result;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * an interface to define the callback for the function runAndCreatePageInfo
     */
    public interface createPageInfoCallback {
        void run(PageInfo pageInfo);
        void onCancle();
    }
}
